Fase 4 CRISP-DM: Modelling PENDIENTE
Fase 5 CRISP-DM: Evaluation PENDIENTE
Fase 6 CRISP-DM: Deployment PENDIENTE
Bibliografía A MEDIAS
Para la realización de este trabajo voy a seguir la metodología estandar CRISP-DM.
Cross Industry Standard Process for Data Mining (CRISP-DM) es un proceso iterativo de creación de software, centrado en el análisis de datos, dividido en 6 fases:
Business Understanding
En esta fase se intenta clarificar el problema a resolver al igual que los objetivos y limitaciones de nuestra solución.
Data Understanding
En esta fase tenemos que obtener nuestros datos, explorarlos y verificar su calidad.
Data Preparation
En esta fase limpiaremos y formatearemos los datos para maximizar su potencial.
Modeling
En esta fase estudiaremos diferentes formas de realizar los modelos de recomendación y diseñare el modelo.
Evaluation
En esta fase estudiaremos el funcionamiento del modelo
Deployment
En esta fase se detallan los pasos de implementación del sistema en un ambiente profesional y las posibles mejoras.

El objetivo de este Trabajo de Fin de Grado es realizar un sistema de recomendación de películas basado en contenidos (Content-based recommender system), con verificación de recomendaciones basadas en las visualizaciones de otros usuarios.
Este trabajo no tiene un fin comercial. De todos modos, su objetivo comercial sería estudiar las técnicas de sistemas de recomendación y proporcionar una solucíon propia a este problema.
La restricción principal de este trabajo es el tiempo.
Debe ser presentado cualquiera de las siguientes tres convocatorias, Febrero, Junio o Septiembre.
El impacto de este trabajo será a nivel académico y de demostración de las tecnologías actuales de ML y de sistemas de codificación del lenguaje natural.
La pagina web de metacritic es un sitio web que recopila reseñas de álbumes de música, videojuegos, películas, programas de televisión y libros.
Para cada categoría de entretenimiento (musica, videojuegos, libros ...), Metacritic dispone de un top donde se encuentran todos los elementos organizados por nota.
En concreto, me voy a centrar en el top de películas (https://www.metacritic.com/browse/movies/score/metascore/all/filtered?page=0).
Este top está dispuesto (actualmente) en 145 páginas, cada uno con entradas correspondientes a 100 películas.
El conjunto de datos ha sido recogido por mi utilizando un programa de webscrapping realizado en python.
En concreto el programa de webscrapping sigue el siguiente esquema:
DiccionarioPeliculas=[]
Para cada numero x de 1 a 145:
pagina= descargarHTMLPagina(x);
Para cada numero y de 1 a 100:
pelicula = descargarHTMLPelicula(y)
datosPelicula=procesarPelicula(pelicula)
DiccionarioPeliculas.añadir(datosPelicula)
Para completar nuestra base de datos, nos interesaría mucho disponer de las reviews específicas de los usuarios para cada película que hemos recogido previamente.
Por lo tanto, usando otro programa de webscrapping realizaríamos la siguiente tarea:
PeliculasDeseadas = DiccionarioPeliculas
DiccionarioReviews=[]
Para cada pelicula en DiccionarioPeliculas:
HTML = descargarHTMLPelicula(pelicula)
RCs = procesarReviewsCriticos(HTML)
RUs = procesarReviewsUsers(HTML)
DiccionarioReviews.añadir(RCs)
DiccionarioReviews.añadir(RUs)
Primero importaré las librerías necesarias y los datasets de películas y reviews que hemos recopilado en el apartado anterior.
import pandas as pd
import seaborn as sns
import plotly.express as px
import plotly.graph_objects as go
# Comment this if the data visualisations doesn't work on your side
%matplotlib inline
dfMovies = pd.read_csv('moviedataset3_11_21.csv')
dfReviews = pd.read_csv('reviewDataSet.csv')
Habiendo importado los datasets a un dataframe de la librería pandas correctamente, usaré el info para mostrar el número de entradas y los atributos de cada dataset.
dfMovies.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 14213 entries, 0 to 14212 Data columns (total 13 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 title 14213 non-null object 1 age_rating 14213 non-null object 2 rating 14213 non-null object 3 rank 14213 non-null int64 4 genre 14213 non-null object 5 director 14213 non-null object 6 year 14213 non-null int64 7 producer 13941 non-null object 8 actor 14213 non-null object 9 runtime 14213 non-null int64 10 description 14210 non-null object 11 img 14213 non-null object 12 url 14213 non-null object dtypes: int64(3), object(10) memory usage: 1.4+ MB
Como podemos ver, disponemos de 13255 entradas con 12 columnas.
Solo 4 de estas columnas son variables discretas, rating, year of release, runtime y rank.
Rating contiene la nota de 0 a 10 impuesta por los usuarios.
Rank contiene la posición 1-15000 en la que esta película (impuesto por metacritic)
Year contiene el año de salida de la película.
Runtime contiene la duración de la película.
Es esperable que los atributos rating y rank esten fuertemente correlados ya que expresan información muy parecida. Esta afirmación la comprobaré mas tarde, ya que si el valor de correlación es muy alto podríamos considerar un solo atributo.
Por otro lado las variables categóricas son las siguientes:
Title: contiene el titulo de la película.
age_rating: indica la edad mínima recomendada para la visualización. Posteriormente haré un análisis para representarla como un entero.
genre: indica el género.
director: nombre del director principal de la película.
producer: nombre de la productora.
actor: nombre del actor principal de la película.
description: parrafo con la descripción de la película.
img: url de la carátula de la película.
url: url de la página de metacritic donde se describe la película.
Por otro lado, la composición de nuestro dataset de reviews es la siguiente:
dfReviews.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 584150 entries, 0 to 584149 Data columns (total 5 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 title 584150 non-null object 1 user 584148 non-null object 2 type 584150 non-null object 3 grade 584150 non-null int64 4 review 584150 non-null object dtypes: int64(1), object(4) memory usage: 22.3+ MB
Como podemos ver tiene 5 atributos.
Title: string que contiene el nombre de la película que se está puntuando.
User: string que contiene el nombre de usuario del autor de la review.
Type: variable que identifica si el autor es un "reviewer profesional" o un usuario de la web.
Grade: nota de la review.
Review: string que contiene la valoración de la película.
fig = px.pie(dfMovies, names=dfMovies["genre"].value_counts().index, values=dfMovies["genre"].value_counts())
fig.update_traces(hoverinfo='value', textinfo='label+percent')
fig.update_layout(autosize=False,width=1000,height=1000,)
fig.show();
Como podemos observar, nuestro dataset está compuesto por películas dispuestas en 22 géneros.
El drama es el género más común por grán mayoría. Seguido por acción Acción, documentales y biografías.
Como no vamos a realizar una predicción de genéneros de películas, en principio, este desbalanceo no presentará un problema en el futuro.
Por otra parte, ratings de edad de las películas se disponen de la siguiente forma:
fig = px.bar(dfMovies, x=dfMovies["age_rating"].value_counts().index, y=dfMovies["age_rating"].value_counts())
fig.update_layout(autosize=False,width=1000,height=300,)
fig.show();
Los años de salida siguen la siguiente distribución
fig = px.histogram(dfMovies, x=dfMovies["year"].value_counts().index, y=dfMovies["year"].value_counts(),nbins=130)
fig.update_layout(autosize=False,width=1000,height=300,)
fig.show();
Como podemos observar, desde el año 2000 a habido un crecimiento lineal en la producción de películas, a excepción de 2 parones en los siguientes años:
prodCounts=pd.DataFrame(dfMovies["producer"].value_counts()).head(30)
test_tree = go.Figure(go.Treemap(
labels = prodCounts["producer"].index,
parents=[""]*len(prodCounts["producer"].index),
values = prodCounts["producer"].values,
textinfo = "label+value"
))
test_tree.show();
Para mostrar el tiempo de ejecución voy a usar un diagrama de "caja y bigotes" o boxplot.
El resultado es el siguiente:
fig = px.box(dfMovies, x="runtime", title="Boxplot representativo del tiempo de ejecución.")
fig.show();
Como podemos observar, la mediana en cuanto a duración es 101 minutos.
Los valores extremos se situan en 62 y 144 minutos. Las duraciones fuera de este rango se consideran datos atípicos o outliers.
También vamos a visualizar los 25 actores principales más comunes.
actorCounts=pd.DataFrame(dfMovies["actor"].value_counts())[1:26]
fig = px.pie(actorCounts, names=actorCounts["actor"].index, values=actorCounts["actor"])
fig.update_layout(autosize=False,width=900,height=490,)
fig.update_traces(hoverinfo='value', textinfo='label+percent')
fig.show();
directorCounts=pd.DataFrame(dfMovies["director"].value_counts()).head(25)
z=list(range(1, 26))
fig = go.Figure(data=[go.Bar(
x=directorCounts["director"].index,
y=directorCounts["director"],
marker=dict(color = z, colorscale='Portland'))
])
fig.update_layout(title_text='Top 25 actores');
Con respecto a las notas de las reviews, voy a realizar un boxplot para representar las notas de los usuarios y los reviewers.
fig = px.box(dfReviews, x="grade",y="type")
fig.show()
Como podemos observar, las notas de los reviewers se encuentran en el rango de 0 a 100 y las de los usuarios, de 0 a 10. Por lo tanto, esto lo tendremos que tener en cuenta en el apartado de data preparation.
reviewcounts=pd.DataFrame(dfReviews["title"].value_counts()).head(40)
test_tree = go.Figure(go.Treemap(
labels = reviewcounts["title"].index,
parents=[""]*len(reviewcounts["title"].index),
values = reviewcounts["title"].values,
textinfo = "label+value"
))
test_tree.show();
Para verificar la calidad de nuestros conjuntos de datos verificaré los siguientes criterios:
Para comprobar la unicidad de nuestros conjuntos de datos podemos utilizar el método de los dataframes de la librería pandas
Pandas.DataFrame.duplicated()
Este método, dado un dataframe, devuelve, para cada una de las entradas, si es única o no.
Agrupando este resultado con la función value_counts() obtenemos el siguiente resultado.
print("Reviews duplicadas")
print(dfReviews.duplicated().value_counts())
print("")
print("Peliculas duplicadas")
print(dfMovies.duplicated().value_counts())
Reviews duplicadas False 583727 dtype: int64 Peliculas duplicadas False 14213 dtype: int64
Por lo tanto, podemos ejecutar la función drop_duplicates() de los dataframes de la librería pandas y eliminar estos valores repetidos.
Si comprobasemos ahora los resultados de la función anterior obtendriamos 0 repetidos.
dfReviews=dfReviews.drop_duplicates()
dfReviews.duplicated().value_counts()
False 583727 dtype: int64
Considero que tanto el dataset de reviews como el de películas son muy completos en cuanto a contenido y numero de entradas. Comparando con los 3 datasets de reviews de películas mas "relevantes" según el buscador de Kaggle:
Aspectos importantes
Algunas reviews incluyen la string "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXx" indicando que esa review tiene spoilers. Este patrón será eliminado del contenido de la review y indicado en una nueva columna de nuestro dataset.
Los tendremos en cuenta en data preparation ### 2.4.4 Consistencia:
Como hemos podido observar en el gráfico anterior, esta columna está muy desbalanceada, a demas tiene un alto contenido de valores nulos. De todos modos, vamos a realizar data wrangling de forma que agrupamos estas variables categóricas en edades como numeros enteros.
print("Edades antes de procesado",dfMovies["age_rating"].unique() )
df_mod=dfMovies.replace(" | Approved", 15)
df_mod=df_mod.replace(" | R", 18)
df_mod=df_mod.replace(' | TV-G', 0)
df_mod=df_mod.replace(' | TV-PG', 15)
df_mod=df_mod.replace(' | PG', 15)
df_mod=df_mod.replace(' | G', 0)
df_mod=df_mod.replace(' | Passed', 0)
df_mod=df_mod.replace(' | Not Rated', -5)
df_mod=df_mod.replace(' | PG-13', 13)
df_mod=df_mod.replace(' | PG-13`', 13)
df_mod=df_mod.replace(' | GP', 0)
df_mod=df_mod.replace(' | M', 15)
df_mod=df_mod.replace(' | M/PG', 15)
df_mod=df_mod.replace(' | TV-MA', 17)
df_mod=df_mod.replace(' | Unrated', -5)
df_mod=df_mod.replace('Not Rated', -5)
df_mod=df_mod.replace(' | TV-14', 14)
df_mod=df_mod.replace(' | NC-17', 17)
df_mod=df_mod.replace(' | NR', -5)
df_mod=df_mod.replace(' | Open', 14)
df_mod=df_mod.replace(' | X', 18)
df_mod=df_mod.replace(' | MA-17', 14)
df_mod=df_mod.replace(' | PG--13', 13)
df_mod=df_mod.replace(' | TV-Y7-FV', 7)
df_mod=df_mod.replace(' | TV-Y7', 7)
print()
print()
print("Edades despues de procesado",df_mod["age_rating"].unique() )
Edades antes de procesado [' | Approved' ' | R' ' | TV-G' ' | TV-PG' ' | PG' ' | G' ' | Passed' ' | Not Rated' ' | PG-13' ' | GP' ' | M' ' | M/PG' ' | TV-MA' ' | Unrated' 'Not Rated' ' | TV-14' ' | NC-17' ' | NR' ' | Open' ' | X' ' | MA-17' ' | PG--13' ' | PG-13`' ' | TV-Y7-FV' ' | TV-Y7'] Edades despues de procesado [15 18 0 -5 13 17 14 7]
fig = px.bar(df_mod, x=df_mod["age_rating"].value_counts().index, y=df_mod["age_rating"].value_counts())
fig.update_layout(autosize=False,width=700,height=500,)
fig.show();
Como podemos observar, hemos eliminado las variables categóricas y el resultado es mucho mas claro.
Seguimos teniendo una gran cantidad de valores sin calificar (valor -1 en el eje x), sin embargo, como la gran mayoría de valores se encuentran entre 13 y 18 creo que sería buena idea sustituirlo por el valor medio.
print(df_mod['age_rating'].loc[df_mod['age_rating'] > -1].value_counts())
print()
print("El valor medio es ",df_mod['age_rating'].loc[df_mod['age_rating'] > 0].mean(axis=0)," por lo tanto susutituiré las entradas no calificadas por 16")
18 4693 13 2442 15 1654 0 348 17 312 14 146 7 2 Name: age_rating, dtype: int64 El valor medio es 16.044112877067793 por lo tanto susutituiré las entradas no calificadas por 16
df_mod=df_mod.replace(-1, 16)
CRISP DM Pete Chapman, Julian Clinton, Randy Kerber, Thomas Khabaza, Thomas Reinartz, Colin Shearer, and Rüdiger Wirth (2000); The CRISP-DM User Guide https://www.the-modeling-agency.com/crisp-dm.pdf
Metacritic top
Beautiful soup documentation
La web donde explican lo de STEM,TOR, proxy...
</font>